使用 TypeScript 通过强大的类型安全增强您的 Express.js 应用程序。本指南涵盖了路由处理程序定义、中间件类型和构建可扩展且可维护的 API 的最佳实践。
TypeScript Express 集成:路由处理程序类型安全
TypeScript 已经成为现代 JavaScript 开发的基石,提供静态类型功能,从而增强代码质量、可维护性和可扩展性。当与流行的 Node.js Web 应用程序框架 Express.js 结合使用时,TypeScript 可以显著提高后端 API 的稳健性。本综合指南探讨了如何利用 TypeScript 在 Express.js 应用程序中实现路由处理程序类型安全,提供了实用的示例和构建面向全球用户的强大且可维护的 API 的最佳实践。
为什么类型安全在 Express.js 中很重要
在 JavaScript 等动态语言中,错误通常在运行时被捕获,这可能导致意外行为和难以调试的问题。TypeScript 通过引入静态类型来解决这个问题,允许您在开发过程中捕获错误,然后再将其引入生产环境。在 Express.js 的上下文中,类型安全对于路由处理程序尤其重要,因为您正在处理请求和响应对象、查询参数和请求正文。不正确地处理这些元素可能导致应用程序崩溃、数据损坏和安全漏洞。
- 尽早检测错误:在开发过程中捕获与类型相关的错误,从而降低运行时出现意外情况的可能性。
- 提高代码可维护性:类型注释使代码更容易理解和重构。
- 增强代码补全和工具:IDE 可以通过类型信息提供更好的建议和错误检查。
- 减少错误:类型安全有助于防止常见的编程错误,例如将不正确的数据类型传递给函数。
设置 TypeScript Express.js 项目
在深入研究路由处理程序类型安全之前,让我们设置一个基本的 TypeScript Express.js 项目。这将作为我们示例的基础。
先决条件
- 已安装 Node.js 和 npm(Node Package Manager)。您可以从官方 Node.js 网站下载它们。确保您拥有最新版本以获得最佳兼容性。
- 代码编辑器,例如 Visual Studio Code,它提供出色的 TypeScript 支持。
项目初始化
- 创建新的项目目录:
mkdir typescript-express-app && cd typescript-express-app - 初始化一个新的 npm 项目:
npm init -y - 安装 TypeScript 和 Express.js:
npm install typescript express - 安装 Express.js 的 TypeScript 声明文件(对类型安全很重要):
npm install @types/express @types/node - 初始化 TypeScript:
npx tsc --init(这将创建一个tsconfig.json文件,该文件配置 TypeScript 编译器。)
配置 TypeScript
打开 tsconfig.json 文件并进行适当的配置。这是一个示例配置:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
需要注意的关键配置:
target:指定 ECMAScript 目标版本。es6是一个不错的起点。module:指定模块代码生成。commonjs是 Node.js 的常见选择。outDir:指定已编译 JavaScript 文件的输出目录。rootDir:指定 TypeScript 源代码文件的根目录。strict:启用所有严格的类型检查选项,以增强类型安全。强烈推荐这样做。esModuleInterop:启用 CommonJS 和 ES Modules 之间的互操作性。
创建入口点
创建一个 src 目录并添加一个 index.ts 文件:
mkdir src
touch src/index.ts
使用基本的 Express.js 服务器设置填充 src/index.ts:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
app.get('/', (req: Request, res: Response) => {
res.send('Hello, TypeScript Express!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
添加构建脚本
将构建脚本添加到您的 package.json 文件中以编译 TypeScript 代码:
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "npm run build && npm run start"
}
现在您可以运行 npm run dev 来构建和启动服务器。
路由处理程序类型安全:定义请求和响应类型
路由处理程序类型安全的核心在于正确定义 Request 和 Response 对象的类型。Express.js 为这些对象提供了泛型类型,允许您指定查询参数、请求正文和路由参数的类型。
基本路由处理程序类型
让我们从一个简单的路由处理程序开始,该处理程序期望将名称作为查询参数:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
interface NameQuery {
name: string;
}
app.get('/hello', (req: Request, res: Response) => {
const name = req.query.name;
if (!name) {
return res.status(400).send('Name parameter is required.');
}
res.send(`Hello, ${name}!`);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
在这个例子中:
Request<any, any, any, NameQuery>定义了请求对象的类型。- 第一个
any表示路由参数(例如,/users/:id)。 - 第二个
any表示响应正文类型。 - 第三个
any表示请求正文类型。 NameQuery是一个接口,它定义了查询参数的结构。
通过定义 NameQuery 接口,TypeScript 现在可以验证 req.query.name 属性是否存在,并且类型为 string。如果您尝试访问不存在的属性或分配错误类型的的值,TypeScript 将标记一个错误。
处理请求正文
对于接受请求正文的路由(例如,POST、PUT、PATCH),您可以在 Request 类型中为请求正文定义一个接口并使用它:
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
const app = express();
const port = 3000;
app.use(bodyParser.json()); // Important for parsing JSON request bodies
interface CreateUserRequest {
firstName: string;
lastName: string;
email: string;
}
app.post('/users', (req: Request, res: Response) => {
const { firstName, lastName, email } = req.body;
// Validate the request body
if (!firstName || !lastName || !email) {
return res.status(400).send('Missing required fields.');
}
// Process the user creation (e.g., save to database)
console.log(`Creating user: ${firstName} ${lastName} (${email})`);
res.status(201).send('User created successfully.');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
在这个例子中:
CreateUserRequest定义了预期请求正文的结构。app.use(bodyParser.json())对于解析 JSON 请求正文至关重要。没有它,req.body将未定义。Request类型现在是Request<any, any, CreateUserRequest>,表明请求正文应符合CreateUserRequest接口。
TypeScript 现在将确保 req.body 对象包含预期的属性(firstName、lastName 和 email)及其类型正确。这大大降低了因不正确的请求正文数据而导致运行时错误的风险。
处理路由参数
对于带有参数的路由(例如,/users/:id),您可以在 Request 类型中为路由参数定义一个接口并使用它:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
interface UserParams {
id: string;
}
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
const users: User[] = [
{ id: '1', firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' },
{ id: '2', firstName: 'Jane', lastName: 'Smith', email: 'jane.smith@example.com' },
];
app.get('/users/:id', (req: Request, res: Response) => {
const userId = req.params.id;
const user = users.find(u => u.id === userId);
if (!user) {
return res.status(404).send('User not found.');
}
res.json(user);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
在这个例子中:
UserParams定义了路由参数的结构,指定id参数应为字符串。Request类型现在是Request<UserParams>,表明req.params对象应符合UserParams接口。
TypeScript 现在将确保 req.params.id 属性存在,并且类型为 string。这有助于防止因访问不存在的路由参数或将其与不正确的类型一起使用而导致的错误。
指定响应类型
虽然专注于请求类型安全至关重要,但定义响应类型也有助于提高代码清晰度并有助于防止不一致。您可以定义您在响应中发回的数据的类型。
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
interface User {
id: string;
firstName: string;
lastName: string;
email: string;
}
const users: User[] = [
{ id: '1', firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' },
{ id: '2', firstName: 'Jane', lastName: 'Smith', email: 'jane.smith@example.com' },
];
app.get('/users', (req: Request, res: Response) => {
res.json(users);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
在这里,Response<User[]> 指定响应正文应为 User 对象数组。这有助于确保您在 API 响应中始终发送正确的数据结构。如果您尝试发送不符合 User[] 类型的数据,TypeScript 将发出警告。
中间件类型安全
中间件函数对于处理 Express.js 应用程序中的横切关注点至关重要。确保中间件中的类型安全与路由处理程序中的类型安全一样重要。
键入中间件函数
TypeScript 中中间件函数的基本结构类似于路由处理程序的结构:
import express, { Request, Response, NextFunction } from 'express';
function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
// Authentication logic
const isAuthenticated = true; // Replace with actual authentication check
if (isAuthenticated) {
next(); // Proceed to the next middleware or route handler
} else {
res.status(401).send('Unauthorized');
}
}
const app = express();
const port = 3000;
app.use(authenticationMiddleware);
app.get('/', (req: Request, res: Response) => {
res.send('Hello, authenticated user!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
在这个例子中:
NextFunction是 Express.js 提供的类型,表示链中的下一个中间件函数。- 中间件函数接受与路由处理程序相同的
Request和Response对象。
增强请求对象
有时,您可能希望在中间件中将自定义属性添加到 Request 对象中。例如,身份验证中间件可能会将 user 属性添加到请求对象中。要以类型安全的方式执行此操作,您需要增强 Request 接口。
import express, { Request, Response, NextFunction } from 'express';
interface User {
id: string;
username: string;
email: string;
}
// Augment the Request interface
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
function authenticationMiddleware(req: Request, res: Response, next: NextFunction) {
// Authentication logic (replace with actual authentication check)
const user: User = { id: '123', username: 'johndoe', email: 'john.doe@example.com' };
req.user = user; // Add the user to the request object
next(); // Proceed to the next middleware or route handler
}
const app = express();
const port = 3000;
app.use(authenticationMiddleware);
app.get('/', (req: Request, res: Response) => {
const username = req.user?.username || 'Guest';
res.send(`Hello, ${username}!`);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
在这个例子中:
- 我们使用全局声明来增强
Express.Request接口。 - 我们向
Request接口添加了一个可选的user属性,类型为User。 - 现在,您可以在路由处理程序中访问
req.user属性,而不会出现 TypeScript 错误。req.user?.username中的?对于处理用户未通过身份验证的情况至关重要,从而防止潜在错误。
TypeScript Express 集成的最佳实践
为了最大限度地利用 TypeScript 在 Express.js 应用程序中的优势,请遵循以下最佳实践:
- 启用严格模式:在您的
tsconfig.json文件中使用"strict": true选项以启用所有严格的类型检查选项。这有助于尽早捕获潜在错误并确保更高水平的类型安全。 - 使用接口和类型别名:定义接口和类型别名来表示数据的结构。这使您的代码更具可读性和可维护性。
- 使用泛型类型:利用泛型类型创建可重用且类型安全的组件。
- 编写单元测试:编写单元测试以验证代码的正确性并确保您的类型注释准确。测试对于维护代码质量至关重要。
- 使用 Linter 和 Formatter:使用 Linter(如 ESLint)和 Formatter(如 Prettier)来强制执行一致的编码样式并捕获潜在错误。
- 避免使用
any类型:尽量减少any类型的使用,因为它会绕过类型检查并违背使用 TypeScript 的目的。仅在绝对必要时使用它,并考虑尽可能使用更具体的类型或泛型。 - 在逻辑上构建您的项目:根据功能将您的项目组织成模块或文件夹。这将提高应用程序的可维护性和可扩展性。
- 使用依赖注入:考虑使用依赖注入容器来管理应用程序的依赖项。这可以使您的代码更易于测试和维护。InversifyJS 等库是热门选择。
Express.js 的高级 TypeScript 概念
使用装饰器
装饰器提供了一种简洁而富有表现力的方式来向类和函数添加元数据。您可以使用装饰器简化 Express.js 中的路由注册。
首先,您需要在您的 tsconfig.json 文件中通过添加 "experimentalDecorators": true 到 compilerOptions 来启用实验性装饰器。
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true
}
}
然后,您可以创建一个自定义装饰器来注册路由:
import express, { Router, Request, Response } from 'express';
function route(method: string, path: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (!target.__router__) {
target.__router__ = Router();
}
target.__router__[method](path, descriptor.value);
};
}
class UserController {
@route('get', '/users')
getUsers(req: Request, res: Response) {
res.send('List of users');
}
@route('post', '/users')
createUser(req: Request, res: Response) {
res.status(201).send('User created');
}
public getRouter() {
return this.__router__;
}
}
const userController = new UserController();
const app = express();
const port = 3000;
app.use('/', userController.getRouter());
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
在这个例子中:
route装饰器将 HTTP 方法和路径作为参数。- 它将装饰的方法注册为与该类关联的路由上的路由处理程序。
- 这简化了路由注册并使您的代码更具可读性。
使用自定义类型保护
类型保护是用于缩小特定范围内变量类型的函数。您可以使用自定义类型保护来验证请求正文或查询参数。
interface Product {
id: string;
name: string;
price: number;
}
function isProduct(obj: any): obj is Product {
return typeof obj === 'object' &&
obj !== null &&
typeof obj.id === 'string' &&
typeof obj.name === 'string' &&
typeof obj.price === 'number';
}
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
const app = express();
const port = 3000;
app.use(bodyParser.json());
app.post('/products', (req: Request, res: Response) => {
if (!isProduct(req.body)) {
return res.status(400).send('Invalid product data');
}
const product: Product = req.body;
console.log(`Creating product: ${product.name}`);
res.status(201).send('Product created');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
在这个例子中:
isProduct函数是一个自定义类型保护,用于检查对象是否符合Product接口。- 在
/products路由处理程序内部,isProduct函数用于验证请求正文。 - 如果请求正文是有效产品,TypeScript 知道在
if块内req.body的类型为Product。
解决 API 设计中的全球性考虑因素
为全球受众设计 API 时,应考虑几个因素以确保可访问性、可用性和文化敏感性。
- 本地化和国际化 (i18n 和 L10n):
- 内容协商:通过基于
Accept-Language标头的内容协商支持多种语言和区域。 - 日期和时间格式:使用 ISO 8601 格式进行日期和时间表示,以避免不同区域的歧义。
- 数字格式:根据用户的语言环境处理数字格式(例如,小数分隔符和千位分隔符)。
- 货币处理:支持多种货币并提供必要的汇率信息。
- 文本方向:适应从右到左 (RTL) 的语言,例如阿拉伯语和希伯来语。
- 内容协商:通过基于
- 时区:
- 在服务器端以 UTC(协调世界时)存储日期和时间。
- 允许用户指定他们喜欢的时区,并在客户端相应地转换日期和时间。
- 使用
moment-timezone等库来处理时区转换。
- 字符编码:
- 使用 UTF-8 编码处理所有文本数据,以支持来自不同语言的各种字符。
- 确保您的数据库和其他数据存储系统配置为使用 UTF-8。
- 可访问性:
- 遵循可访问性指南(例如,WCAG)以使您的 API 易于残疾用户访问。
- 提供易于理解的清晰和描述性错误消息。
- 在您的 API 文档中使用语义 HTML 元素和 ARIA 属性。
- 文化敏感性:
- 避免使用可能并非所有用户都能理解的特定文化参考、惯用语或幽默。
- 注意交流风格和偏好的文化差异。
- 考虑您的 API 对不同文化群体可能产生的影响,并避免延续刻板印象或偏见。
- 数据隐私和安全:
- 遵守数据隐私法规,例如 GDPR(通用数据保护条例)和 CCPA(加州消费者隐私法)。
- 实施强大的身份验证和授权机制来保护用户数据。
- 对传输和静态的敏感数据进行加密。
- 向用户提供对其数据的控制权,并允许他们访问、修改和删除其数据。
- API 文档:
- 提供全面且组织良好的 API 文档,易于理解和浏览。
- 使用 Swagger/OpenAPI 等工具生成交互式 API 文档。
- 包含多种编程语言的代码示例,以满足不同受众的需求。
- 将您的 API 文档翻译成多种语言,以覆盖更广泛的受众。
- 错误处理:
- 提供具体且信息丰富的错误消息。避免使用通用的错误消息,例如“出错了”。
- 使用标准 HTTP 状态代码来指示错误类型(例如,400 表示错误的请求,401 表示未授权,500 表示内部服务器错误)。
- 包括可用于跟踪和调试问题的错误代码或标识符。
- 在服务器端记录错误以进行调试和监视。
- 速率限制:实施速率限制以保护您的 API 免受滥用并确保公平使用。
- 版本控制:使用 API 版本控制来允许向后兼容的更改并避免破坏现有客户端。
结论
TypeScript Express 集成显着提高了后端 API 的可靠性和可维护性。通过在路由处理程序和中间件中利用类型安全,您可以在开发过程中尽早捕获错误,并为全球受众构建更强大、更可扩展的应用程序。通过定义请求和响应类型,您可以确保您的 API 遵守一致的数据结构,从而降低运行时错误的发生可能性。请记住遵循最佳实践,例如启用严格模式、使用接口和类型别名以及编写单元测试,以最大限度地利用 TypeScript 的优势。务必考虑全球因素,如本地化、时区和文化敏感性,以确保您的 API 在全球范围内可访问且可用。